﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Xml;
using Kent.Boogaart.HelperTrinity;
using Kent.Boogaart.HelperTrinity.Extensions;
using NVelocity;

namespace Kent.Boogaart.Gauntlet.InputTranslators
{
	/// <summary>
	/// An implementation of <see cref="IInputTranslator"/> that allows output generators to access the contents of an XML input
	/// document.
	/// </summary>
	/// <remarks>
	/// <para>
	/// The <c>XmlInputTranslator</c> is an input translator that takes an XML document as input and exposes it for consumption by an
	/// output generator. The root element of the document is used as a key to store the document contents in the context.
	/// </para>
	/// <para>
	/// Implementation note: for now, the objects placed in the context by this input translator are specifically designed to work with
	/// NVelocity (by implementing NVelocity's <c>IDuck</c> interface).
	/// </para>
	/// </remarks>
	public class XmlInputTranslator : IInputTranslator
	{
		/// <summary>
		/// Translates the input (which must be a valid XML document) into a set of objects that can be consumed by an output generator.
		/// </summary>
		/// <param name="context">
		/// The context.
		/// </param>
		public void Translate(Context context)
		{
			context.AssertNotNull("context");
			XmlDocument document = new XmlDocument();

			try
			{
				document.Load(context.InputFile.FullName);
			}
			catch (XmlException ex)
			{
				ExceptionHelper.Throw("InvalidXml", ex, context.InputFile.FullName);
			}

			context.State[document.DocumentElement.Name] = new XmlNodeSet(document.DocumentElement, document.DocumentElement.Name);
		}

		private sealed class XmlNodeSet : IEnumerable<XmlNodeSet>, IDuck
		{
			private readonly IList<XmlNode> _xmlNodes;
			private readonly string _expression;

			public XmlNodeSet(XmlNode xmlNode, string expression)
			{
				Debug.Assert(xmlNode != null);
				Debug.Assert(expression != null);

				_xmlNodes = new List<XmlNode>(1);
				_xmlNodes.Add(xmlNode);
				_expression = expression;
			}

			public XmlNodeSet(IList<XmlNode> xmlNodes, string expression)
			{
				Debug.Assert(xmlNodes != null);
				Debug.Assert(expression != null);

				_xmlNodes = xmlNodes;
				_expression = expression;
			}

			public XmlNodeSet(XmlNodeList xmlNodes, string expression)
			{
				Debug.Assert(xmlNodes != null);
				Debug.Assert(expression != null);

				_xmlNodes = new List<XmlNode>(xmlNodes.Count);
				_expression = expression;

				foreach (XmlNode xmlNode in xmlNodes)
				{
					_xmlNodes.Add(xmlNode);
				}
			}

			public object GetInvoke(string propName)
			{
				string newExpression = string.Concat(_expression, ".", propName);

				if (_xmlNodes.Count > 1)
				{
					return null;
				}

				List<XmlNode> matches = new List<XmlNode>();

				//search attributes first
				foreach (XmlAttribute xmlAttribute in _xmlNodes[0].Attributes)
				{
					if (string.Equals(propName, xmlAttribute.Name))
					{
						matches.Add(xmlAttribute);
					}
				}

				if (matches.Count == 0)
				{
					//no attribute found, search child nodes next (attributes take precedence over like-named child elements)
					foreach (XmlNode xmlNode in _xmlNodes[0].ChildNodes)
					{
						if (string.Equals(propName, xmlNode.Name))
						{
							matches.Add(xmlNode);
						}
					}
				}

				if (matches.Count == 0)
				{
					return null;
				}

				//found matches, so wrap up those matches for further access
				return new XmlNodeSet(matches, newExpression);
			}

			public void SetInvoke(string propName, object value)
			{
				throw new NotSupportedException();
			}

			public object Invoke(string method, params object[] args)
			{
				if (_xmlNodes.Count != 1)
				{
					//all these methods require exactly one child against which to operate
					return null;
				}

				if (method == "Name")
				{
					if (args.Length != 0)
					{
						return null;
					}

					return _xmlNodes[0].Name;
				}
				else if (method == "Count")
				{
					if (args.Length > 1 || (args.Length == 1 && !(args[0] is string)))
					{
						return null;
					}

					string xpath = "*";

					if (args.Length == 1)
					{
						xpath = args[0] as string;
					}

					return _xmlNodes[0].SelectNodes(xpath).Count;
				}
				else if (method == "Children")
				{
					if (args.Length > 1 || (args.Length == 1 && !(args[0] is string)))
					{
						return null;
					}

					string xpath = "*";

					if (args.Length == 1)
					{
						xpath = args[0] as string;
					}

					return new XmlNodeSet(_xmlNodes[0].SelectNodes(xpath), string.Concat(_expression, ".Children('", xpath, "')"));
				}

				return null;
			}

			public override string ToString()
			{
				if (_xmlNodes.Count == 1)
				{
					if (_xmlNodes[0] is XmlAttribute)
					{
						return _xmlNodes[0].Value;
					}
					else
					{
						return _xmlNodes[0].InnerXml;
					}
				}
				else
				{
					return string.Format(CultureInfo.InvariantCulture, "${0}: {1} children", _expression, _xmlNodes.Count);
				}
			}

			public IEnumerator<XmlNodeSet> GetEnumerator()
			{
				foreach (XmlNode xmlNode in _xmlNodes)
				{
					List<XmlNode> childList = new List<XmlNode>(1);
					childList.Add(xmlNode);
					yield return new XmlNodeSet(childList, _expression);
				}
			}

			System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
			{
				return GetEnumerator();
			}
		}
	}
}